/**
 * @file
 * The xine-related code is modified from the source code in
 * "The xine hacker's guide" (http://xinehq.de/index.php/hackersguide#AEN80)
 */

#include <stdio.h>
#include <string>
#include <iostream>

#include "Decoder_xine.h"
#include "Debug.h"

#define INPUT_MOTION (ExposureMask | ButtonPressMask | KeyPressMask | \
		ButtonMotionMask | StructureNotifyMask |              \
		PropertyChangeMask | PointerMotionMask)

#define DEFAULT_WIDTH 320
#define DEFAULT_HEIGHT 240

/* xine and X11 related static variables */
static int xpos, ypos, width, height;
static double pixel_aspect;

/* xine related static functions */
static void event_listener( void *user_data, const xine_event_t * event );
static void dest_size_cb( void *data, int video_width, int video_height,
	      double video_pixel_aspect, int *dest_width, int *dest_height,
	      double *dest_pixel_aspect );
static void frame_output_cb( void *data, int video_width, int video_height,
	         double video_pixel_aspect, int *dest_x, int *dest_y,
		 int *dest_width, int *dest_height,
		 double *dest_pixel_aspect, int *win_x, int *win_y );

/* Thread for xevent */
static void *thread_get_xevent( void *decoder );

/**
 * @brief Contructor of Decoder_xine
 *
 * Invoke init() to initialize Decoder_xine.
 */
Decoder_xine::Decoder_xine()
{
	init();
}

/**
 * @brief Initialize xine and playback windows.
 *
 * init() initializes the xine engine and creates two playback windows
 * and consists of 3 parts:
 *
 * -# Load xine config and start the xine engine.
 * -# Create two windows and make one of them visible later.
 *     - window 0: Set to default width and height. (window 0 will be resized
 *                 in Decoder_xine::init_play() if necessary.)
 *     - window 1: Set the width and height of the window equal to the width and
 *                 height of the screen.(Fullscreen)
 * -# Initialize the xine engine
 *     -# Fill in the xine visual struct
 *         - vis.display
 *         - vis.screen
 *         - vis.d
 *         - vis.dest_size_cb
 *         - vis.frame_output_cb
 *         - pixel_aspect
 *     -# Open xine video/audio ports.
 *     -# Open a xine stream connect to a/v ports and register a xine event
 *        listener.
 *     -# Connect video port to playback window.
 *
 */
void
Decoder_xine::init()
{
	char configFile[2048];

	x11_visual_t vis;
	double res_h, res_v;

	display = XOpenDisplay (NULL);
	screen = XDefaultScreen (display);
	xpos = 0;
	ypos = 0;
	width = DEFAULT_WIDTH;
	height = DEFAULT_HEIGHT;

	/* Initialize private variables */
	ao_driver = "auto"; // set "auto" for audio output driver
	vo_driver = "auto"; // set "auto" for video output driver
	xine = NULL;
	stream = NULL;
	vo_port = NULL;
	ao_port = NULL;
	event_queue = NULL;

	/* --- Load xine config file and initialize xine. --- */
	xine = xine_new(); // Create a new xine engine.
	snprintf(configFile, sizeof (configFile), "%s%s",
			xine_get_homedir(), "/.xine/config");
	xine_config_load (xine, configFile); // Load the config of xine
	xine_init(xine); // Start the xine engine.

	/* --- Initialize Playback Windows --- */
	/* [Lock] Display */
	XLockDisplay( display );

	/* Set default window to window 0(Normal Playback Window). */
	fullscreen = 0;

	/* Create 2 windows, one for normal playback and the other for
	 * fullscreen mode. */
	/* window 0: Normal Playback Window */
	window[0] =
		XCreateSimpleWindow (display,
				XDefaultRootWindow( display ),
				xpos,
				ypos,
				width,
				height,
				1, 0, 0);
	/* window 1: Fullscreen */
	window[1] =
		XCreateSimpleWindow (display,
				XDefaultRootWindow( display ),
				0,
				0,
				DisplayWidth( display, screen ),
				DisplayHeight( display, screen ),
				0, 0, 0);

	XSelectInput( display, window[0], INPUT_MOTION );
	XSelectInput( display, window[1], INPUT_MOTION );

	res_h = (DisplayWidth (display, screen) * 1000 /
			DisplayWidthMM (display, screen));
	res_v = (DisplayHeight (display, screen) * 1000 /
			DisplayHeightMM (display, screen));


	/* [Sync] Display */
	XSync (display, False);

	/* [Unlock] Display */
	XUnlockDisplay (display);

	/* --- Initialize xine engine --- */
	/* Fill in the xine visual struct. */
	vis.display = display;
	vis.screen = screen;
	vis.d = window[fullscreen];
	vis.dest_size_cb = dest_size_cb;
	vis.frame_output_cb = frame_output_cb;
	vis.user_data = NULL;
	pixel_aspect = res_v / res_h;

	/* Open xine ports. */
	vo_port = xine_open_video_driver(xine, vo_driver.c_str(),
			XINE_VISUAL_TYPE_X11, (void *) &vis);
	ao_port = xine_open_audio_driver(xine, ao_driver.c_str(), NULL);

	/* Open a xine stream connected to ao/vo ports. */
	stream = xine_stream_new(xine, ao_port, vo_port);
	/* Hook an event handler into the streams events. */
	event_queue = xine_event_new_queue(stream);
	xine_event_create_listener_thread(event_queue, event_listener, NULL);

	/* Make the video window visible to xine. */
	xine_port_send_gui_data(vo_port, XINE_GUI_SEND_DRAWABLE_CHANGED,
			(void *) window[fullscreen]);
	xine_port_send_gui_data(vo_port, XINE_GUI_SEND_VIDEOWIN_VISIBLE,
			(void *) 1);
}

/**
 * @brief Destructor of Decoder_xine
 *
 * Close xine engine and X related resources.
 */
Decoder_xine::~Decoder_xine()
{
	/* Close the stream of xine and the audio/video ports. */
	xine_event_dispose_queue (event_queue);
	xine_dispose (stream);
	xine_close_audio_driver (xine, ao_port);
	xine_close_video_driver (xine, vo_port);

	/* Close X related resources. */
	XLockDisplay (display);
	XUnmapWindow (display, window[fullscreen]);
	XDestroyWindow (display, window[0]);
	XDestroyWindow (display, window[1]);
	XUnlockDisplay (display);

	XCloseDisplay (display);

	/* Shutdown the xine engine. */
	xine_exit (xine);
}

/* ****************** */
/* 5 Basic Operations */
/* ****************** */
/**
 * @brief Decoder Basic Operation: open
 *
 * This function validates the name of the media file and assigns the variable
 * 'mediaFileName' to the parameter 'fileName'.
 *
 * @param fileName A constant string contains the name of the media
 *                 file to be played.
 *
 * @retval >=0 Success!
 * @retval <0 An error occurred.
 */
int
Decoder_xine::open(const string fileName)
{
	first_play = true;

	/* Set the file name for the decoder */
	if( !fileName.empty() ){
		mediaFileName = fileName;
		return 1;
	}
	return -1;
}

/**
 * @brief Initialization function for play()
 *
 * init_play() passes the media file name to the xine engine and detects
 * the original height and width for the video and resizes the play back
 * window if necessary.
 *
 * @retval >=0 Success!
 * @retval <0  Xine fails to open the media file.
 */
int
Decoder_xine::init_play()
{
	int video_width, video_height;

	/* Assign the default window to window 0. */
	fullscreen = 0;

	/* Use the xine engine to open the media file. */
	if (!xine_open( stream, mediaFileName.c_str() )){
		string errMsg = "File Open Error:";
		errMsg += mediaFileName;
		DebugMsg::errMsgOut( DBG_LV_FATAL, "Decoder_xine", "init_play",
				errMsg.c_str());
		return -1;
	}

	/* --- Get the width and height --- */
	/* If both vidth and height are 0, then the media file is a audio file.*/
	video_width = xine_get_stream_info(stream, XINE_STREAM_INFO_VIDEO_WIDTH);
	video_height = xine_get_stream_info(stream, XINE_STREAM_INFO_VIDEO_HEIGHT);

	/* --- Initialization for Audio or Video --- */
	if( video_width == 0 || video_height == 0 ){
		isVideo = false;
	}
	else{
		isVideo = true;
		/* [Lock] Display */
		XLockDisplay( display );
		width = video_width;
		height = video_height;
		/* Resize window 0. */
		XResizeWindow( display, window[0], width, height );
		/* Make window 0 visible. */
		XMapRaised( display, window[fullscreen] );
		/* [Unlock] Display */
		XUnlockDisplay( display );
	}

	return 1;
}

/**
 * @brief Decoder Basic Operation: play
 *
 * - For audio files:
 * 	- Invoke xine_play() to start the playback at specific position.
 * - For video files:
 *      - if first_play == true
 *          -# Invoke init_play() to query the width and height of the
 *             video and resize the playback window.
 *          -# Invoke xine_play() to start the playback at specific
 *             position.
 *          -# Create a xevent handling thread: thread_get_xevent().
 *      - otherwise
 *          - Invoke xine_play() to start the playback at specific position.
 *
 * @param msec The start position to playback
 *
 * @retval >=0 Success!
 * @retval <0  An error occurred.
 */
int
Decoder_xine::play(const unsigned int msec)
{
	/* Validate the file name. */
	if( mediaFileName.empty() ){
		DebugMsg::errMsgOut( DBG_LV_FATAL, "Decoder_xine", "play",
				"Empty Filename");
		return -1;
	}

	/* Invoke init_play() to resize playback window. */
	if( first_play && init_play() < 0 ){
		DebugMsg::errMsgOut( DBG_LV_FATAL, "Decoder_xine", "play",
				"Failed to init_play().");
		return -1;
	}

	/* Start to play the media file at the assigned position. */
	if( !xine_play(stream, 0, msec) ){
		DebugMsg::errMsgOut( DBG_LV_FATAL, "Decoder_xine", "play",
				"Failed to xine_play().");
		return -1;
	}

	/* Start the xevent handling thread. */
	if( isVideo && first_play ){
		pthread_create( &event_thread, NULL, thread_get_xevent,
				(void *)this );
	}

	/* Set first_play to "false" to avoid double initialization. */
	first_play = false;

	return 1;
}

/**
 * @brief Decoder Basic Operation: pause
 *
 * pause() suspends the playback. Because xine supports 'pause',
 * pause() uses xine_set_param() to set the playback speed to
 * XINE_SPEED_PAUSE.
 *
 */
int
Decoder_xine::pause()
{
	int ret;
	/* Pause the playback. */
	ret = xine_get_param(stream, XINE_PARAM_SPEED);
	if( ret != XINE_SPEED_PAUSE ){
		xine_set_param(stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE);
	}

	return 1;
}

/**
 * @brief Decoder Basic Operation: resume
 *
 * resume() resumes the playback from pause by set the playback speed
 * to XINE_SPEED_NORMAL.
 *
 */
int
Decoder_xine::resume()
{
	int ret;
	/* Resume the playback. */
	ret = xine_get_param(stream, XINE_PARAM_SPEED);
	if( ret == XINE_SPEED_PAUSE ){
		xine_set_param(stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL);
	}
	return 1;
}

/**
 * @brief Decoder Basic Operation: stop
 *
 * resume() resumes the playback from pause by set the playback speed
 * to XINE_SPEED_NORMAL.
 *
 */
int
Decoder_xine::stop()
{
	/* Stop the playback and close the stream. */
	xine_stop(stream);
	xine_close (stream);

	/* Close X related resource. */
	if( isVideo ){
		/* Make the playback windows invisible. */
		XLockDisplay( display );
		XUnmapWindow( display, window[fullscreen] );
		XSync( display, false );
		XUnlockDisplay( display );
		/* Wait for the end of the xevent thread. */
		pthread_join( event_thread, NULL );
	}

	/* Reset first_play. */
	first_play = true;

	return 1;
}

/**
 * @brief Toggle the fullscreen switch
 *
 * Raise the normal playback or fullscreen window and unmap the other.
 * Redirect the output of xine engine to the raised window.
 *
 * @retval >=0 Success!
 * @retval <0  An error occurred.
 */
int
Decoder_xine::fullscreen_sw()
{
	Window win_tmp;

	if( mediaFileName.empty() || stream == NULL ||
	    xine_get_status(stream) != XINE_STATUS_PLAY ){
		DebugMsg::errMsgOut( DBG_LV_FATAL, "Decoder_xine", "fullscreen_sw",
				"Invalid stream");
		return -1;
	}

	/* --- Switch the windows. --- */
	/* [Lock] Display */
	XLockDisplay( display );

	/* Make the current window invisible. */
	XUnmapWindow( display, window[fullscreen] );
	/* Switch fullscreen. (0->1 or 1->0) */
	fullscreen = !fullscreen;

	/* Set the property of window 1. (Fullscreen) */
	if(fullscreen){
		Atom Full = XInternAtom(display,"_NET_WM_STATE_FULLSCREEN", False);
		XChangeProperty (display, window[1],
				XInternAtom (display, "_NET_WM_STATE", False),
				XA_ATOM, 32, PropModeReplace,
				(unsigned char *)&Full, 1);
	}

	/* Make the other window visible. */
	XMapRaised( display, window[fullscreen] );

	XSync( display, false );

	/* Adjust the position of window[fullscreen]. */
	XTranslateCoordinates( display,
			window[fullscreen],
			DefaultRootWindow( display ),
			0, 0,
			&xpos, &ypos,
			&win_tmp );

	/* [Unlock] Display */
	XUnlockDisplay( display );

	/* --- Redirect the video data. --- */
	xine_port_send_gui_data( vo_port, XINE_GUI_SEND_DRAWABLE_CHANGED,
			(void *) window[fullscreen] );

	return 1;
}

/* ********************** */
/* Supplemental Functions */
/* ********************** */
/**
 * @brief Meida file filters for xine engine
 *
 * @return The filter string for media file
 * */
const string
Decoder_xine::file_filter() const
{
	string v_filter("*.avi *.wmv *.asf *.mpg *.ogm *.mkv");
	string a_filter("*.wav *.wma *.mp3 *.ogg");
	string ret;

	ret = v_filter + " ";
	ret += a_filter;

	return ret;
}

/**
 * @brief Indicate the current status of the decoder.
 *
 * @return DECODER_IDLE, DECODER_STOP, DECODER_PLAY, or DECODER_QUIT
 */
DECStatus
Decoder_xine::getStatus() const
{
	switch( xine_get_status(stream) ){
		case XINE_STATUS_IDLE:
			return DECODER_IDLE;
		case XINE_STATUS_STOP:
			return DECODER_STOP;
		case XINE_STATUS_PLAY:
			return DECODER_PLAY;
		case XINE_STATUS_QUIT:
			return DECODER_QUIT;
		default:
			break;
	}
	return DECODER_ERR;
}

/**
 * @brief Get the length of the media stream.
 *
 * Use xine_get_pos_length() to get the length of the media stream
 * (in milisecond).
 *
 * @return The lenght of the media stream
 */
int
Decoder_xine::getMediaLength() const
{
	int pos_stream, pos_time, length;
	int ret;

	ret = xine_get_pos_length(stream, &pos_stream, &pos_time, &length);

	if( ret > 0 )
		return length;

	return 0;
}

/**
 * @brief Get the current playback position.
 *
 * Use xine_get_pos_length() to get the current playback position
 * (in millisecond).
 *
 * @return The current playback position
 */
int
Decoder_xine::getCurrentPos() const
{
	int pos_stream, pos_time, length;
	int ret;

	ret = xine_get_pos_length(stream, &pos_stream, &pos_time, &length);

	if( ret > 0 ){
		return pos_time;
	}

	return 0;

}

/**
 * @brief Get the information of the media file.
 *
 * getMediaInfo() uses xine_get_meta_info(stream, XINE_META_INFO_TITLE)
 * to retrieve the title of the media file and return it to the caller.
 *
 * @return A string contains the title of the media file if available.
 *         Otherwise, it returns the file name.
 */
const string
Decoder_xine::getMediaInfo() const
{
	const char *meta_info;
	string output, input;
	string::reverse_iterator str_riter;
	int i = 0, pos;

	/* Get meta info: Title. */
	meta_info = xine_get_meta_info(stream, XINE_META_INFO_TITLE);
	if( meta_info != NULL )
		output = meta_info;
	else{
		/* Extract the file name from the full path. */
		input = mediaFileName;
		str_riter = input.rbegin();
		for(; str_riter < input.rend(); str_riter++){
			if( *str_riter != '/' )
				i++;
			else
				break;
		}
		pos = input.length() - i;
		output = mediaFileName.substr( pos, i );
	}

	return output;
}

/**
 * @brief Get the pointer "display" in Decoder_xine.
 *
 * This function simply returns the pointer "display" since the xevent
 * handling thread needs to change the attributes of "display".
 *
 * @return The pointer "display" in the class 'Decoder_xine'
 */
Display *
Decoder_xine::getDisplay() const
{
	return display;
}

/* ********************************* */
/* Functions required by xine or X11 */
/* ********************************* */

/* --- Xine related functions --- */
/**
 * @brief Xine Event Listener
 *
 * @param user_data Data from user
 * @param event Xine defined events
 */
static void
event_listener (void *user_data, const xine_event_t * event)
{
	switch (event->type) {
/* Handle any xine related event here. */
/*
		case XINE_EVENT_UI_PLAYBACK_FINISHED:
		break;
*/
        }
}

/* This will be called by xine, if it wants to know the target size of a frame */
static void
dest_size_cb( void *data, int video_width, int video_height,
	      double video_pixel_aspect, int *dest_width, int *dest_height,
	      double *dest_pixel_aspect )
{
	*dest_width = width;
	*dest_height = height;
	*dest_pixel_aspect = pixel_aspect;
}

/* This will be called by xine when it's about to draw the frame */
static void
frame_output_cb( void *data, int video_width, int video_height,
	         double video_pixel_aspect, int *dest_x, int *dest_y,
		 int *dest_width, int *dest_height,
		 double *dest_pixel_aspect, int *win_x, int *win_y )
{
	*dest_x = 0;
	*dest_y = 0;
	*win_x = xpos;
	*win_y = ypos;
	*dest_width = width;
	*dest_height = height;
	*dest_pixel_aspect = pixel_aspect;
}

/* --- X11 related functions --- */
/**
 * @brief Handler for expose event (Redraw the playback window.)
 */
void
Decoder_xine::expose_handler( XEvent xevent )
{
	/* Notify the xine engine to redraw the playback window. */
	xine_port_send_gui_data( vo_port,
			         XINE_GUI_SEND_EXPOSE_EVENT,
				 &xevent );
}

/**
 * @brief Thread for xevent handling
 *
 * This thread exists only when the state of decoder is in DECODER_PLAY.
 * We don't have to handle xevents unless the decoder has something to output.
 * Xevent handling thread mainly deals with 3 kinds of xevents:
 *
 *     -# Key Pressing
 *         - Catch the key pressing event and interpret the key code and
 *           invoke the function responding to the key code.
 *     -# Expose Event
 *         - The X server generates Expose events to the client when the contents
 *           of window regions have been lost. Therefore, we invoke
 *           Decoder_xine::expose_handler() to redraw the playback window.
 *     -# Resizing
 *         - When a window has been resized, the server generates a event to
 *           notify the client. When the handling thread gets the resizing event,
 *           it notify the decoder that the playback window is resize and the
 *           decoder needs to resize it output to fit the new window size.
 *
 */
static void *
thread_get_xevent( void *decoder )
{
	Decoder_xine *dec_xine = (Decoder_xine *)decoder;
	Display *display = dec_xine->getDisplay();

	while( dec_xine->getStatus() == DECODER_PLAY ){
		XEvent xevent;
		int got_event = 0;

		/* Check whether there exists a event. */
		XLockDisplay( display );
		got_event = XPending( display );
		if( got_event ){
			XNextEvent( display, &xevent );
		}
		XUnlockDisplay( display );

		if( !got_event ){
			xine_usec_sleep (20000);
			continue;
		}

		/* --- Interpret the pending event. --- */
		switch( xevent.type ){
			/* CASE 1: Key Pressing */
			case KeyPress:
				XKeyEvent kevent;
				KeySym ksym;
				char kbuf[256];
				int len;

				kevent = xevent.xkey;
				XLockDisplay( display );
				len = XLookupString( &kevent, kbuf, sizeof(kbuf),
						&ksym, NULL );
				XUnlockDisplay( display );

				/* Interpret the key code. */
				switch( ksym ){
					/* Toggle the fullscreen switch. */
					case XK_f:
					case XK_F:
						dec_xine->fullscreen_sw();
						break;
					/* ADD other hotkeys here!!! */
					/* case XK_...:
					 * 	....
					 * 	break; */
				}

				break;
			/* CASE 2: Expose Event (Redraw the related window.)*/
			case Expose:
				if (xevent.xexpose.count != 0)
					break;
				/* Invoke the decoder expose handler. */
				dec_xine->expose_handler( xevent );
				break;
			/* CASE 3: Windows Size Changing */
			case ConfigureNotify:
				XConfigureEvent *cev = (XConfigureEvent *) & xevent;
				Window win_tmp;

				/* Change the playback window size through
				 * dest_size_cb() and frame_output_cb() */
				width = cev->width;
				height = cev->height;

				/* Change the coordinate of the playback window */
				if( (cev->x == 0) && (cev->y == 0) ){
					XLockDisplay( display );
					XTranslateCoordinates( display,
						cev->window,
						DefaultRootWindow(cev->display),
						0, 0,
						&xpos, &ypos,
						&win_tmp );
					XUnlockDisplay( display );
				}
				else{
					/* Let frame_output_cb() to handle the
					 * change of coordinate. */
					xpos = cev->x;
					ypos = cev->y;
				}

				break;
		}
	}
}
